<?php
/**
 * Пример фразы: проект техника техника OR проект "новый проект" -проектор
 * 2010.08.29 - версия для MySQL FULLTEXT удаленна, нет смысла поддерживать
 * 2010.08.30 - язык поисковых запросов
 */

require_once $GLOBALS['tenders']['conf']['libdir'] . '/morf.php';

/**
 * модель для поиска
 *
 */
class multitender_model_search extends multitender_model {
    public     $search = array();
    public     $search_orig = array();
    public     $search_query = array();
    public     $search_feed = array();
    /**
     * Ошибка поиска
     * @var string
     */
    public     $search_error = '';
    /**
     * Разбор поискового запроса при очистке
     * @var array
     */
    private    $clear_search_main;

    public     $search_id_start = null;
    public     $search_id_end = null;
    public     $debug = false;
    /**
     * @var ADOConnection
     */
    public     $sphinx = null;
    public     $sphinx_big_number = 1000000000;
    // параметры пяти похожих
    public     $similar_sphinx_date = 100;
    public     $similar_sphinx_weight = 100;
    public     $similar_sphinx_days = 25;
    public     $similar_sphinx_ranker = 'wordcount';
    // время кэша
    public     $memcache_total = 420; // 7*60
    public     $memcache_items = 60;  // 60
    // время выполнения
    public     $mtime = null;
    public     $mtime_mysql = null;
    public     $mtime_sphinx = null;
    /**
     * кэш
     * @var array
     */
    protected  $all_data;

    private $onpage_vals = array(10, 25, 50, 100, 200, 500);

    function __construct($search = null) {
        parent::__construct();

        if (is_null($search)) {
            $search = @$_REQUEST['search'];
            $this->search = $search ? $search : array();
        } else {
            $this->SetSearch($search);
        }

        // TODO подключаться только при реальной необходимости - нет лишних коннектов
        $this->sphinx        = ADONewConnection($this->conf['db_conf']['sphinx']['dsn']);      
        $this->sphinx->debug = $this->conf['db_conf']['sphinx']['debug'];

        $this->memcache_obj = &$GLOBALS['memcached_res'];

        $this->clear_search();

        $this->all_data = multitender_model_data_common::singleton()->get_data();
    }

    /**
     * очишаем параметры поиска
     */
    public function clear_search() {
        mb_internal_encoding('UTF-8');

        $this->search_orig = $this->search;

        if (isset($this->search['main'])) {
            // FIXME $text = mb_strtolower($text);
            $this->clear_search_main = $this->clear_search_main($this->search['main']);
            $this->search['main'] = $this->clear_search_main['clear'];
            if (empty($this->search['main'])) {
                unset($this->search['main']);
            }
        }

        $this->clear_search_array_int('okr');
        $this->clear_search_array_int('reg');
        $this->clear_search_array_int('type');
        $this->clear_search_array_int('sector');
        $this->clear_search_array_int('customer');
        $this->clear_search_array_int('site');
        $this->clear_search_array_int('rubric');

        if (isset($this->search['price_min'])) {
            $this->search['price_min'] = (int) $this->search['price_min'];
            if (!($this->search['price_min'] > 0)) {
                unset($this->search['price_min']);
            }
        }

        if (isset($this->search['price_max'])) {
            $this->search['price_max'] = (int) $this->search['price_max'];
            if (!($this->search['price_max'] > 0)) {
                unset($this->search['price_max']);
            }
        }

        if (isset($this->search['price_min']) && isset($this->search['price_max'])) {
            if ($this->search['price_min'] > $this->search['price_max']) {
                unset($this->search['price_min']);
                unset($this->search['price_max']);
            }
        }

        if (isset($this->search['date_min'])) {
            if (!preg_match('/\d{2}.\d{2}.\d{4}/', $this->search['date_min'])) {
                unset($this->search['date_min']);
            }
        }

        if (isset($this->search['date_max'])) {
            if (!preg_match('/\d{2}.\d{2}.\d{4}/', $this->search['date_max'])) {
                unset($this->search['date_max']);
            }
        }
    }

    private function clear_search_array_int($what) {
        if (isset($this->search[$what])) {
            $array = array();
            if (is_array($this->search[$what])) {
                foreach ($this->search[$what] as $k => $v) {
                    $id = (int) $k;
                    if ($id) {
                        $array[$id] = 'on';
                    }
                }
                if ($what=="type" && isset($this->search['type']['gozall'])) {
                    $array['gozall'] = 'on';
                }
            } else {
                $id = (int) $this->search[$what];
                if ($id) {
                    $array[$id] = 'on';
                }
            }
            if (!empty($array)) {
                $this->search[$what] = $array;
            } else {
                unset($this->search[$what]);
            }
        }
    }

    // очишаем параметры поиска для фидов
    public function clear_search_feed() {
        $this->clear_search();

        $white = array('main', 'okr', 'reg', 'type', 'customer', 'param_or', 'param_no_customer', 'param_no_null_price', 'rubric', 'price_min', 'price_max', 'date_min', 'date_max');

        foreach ($white as $key) {
            if (isset($this->search[$key])) {
                $this->search_feed[$key] = $this->search[$key];
            }
        }

        $this->search = $this->search_feed;
    }


    /**
     * Разбор поискового запроса и очистка его
     * "в скобках" (OR -нет)  =>   <{> скобках <}> <(> <OR> <NOT> нет <)>
     * @param string $search_main
     * @return array
     */
    private function clear_search_main($search_main) {
        $return = array(
                'src'  => $search_main,
                'clear'  => '',
                'tag_form' => '',
                'stages' => array(),
                'word_in_quote' => array(),
                'word_out_quote' => array(),
        );

        // преобразуем все лишнее в пробелы
        $search_main = trim(preg_replace(
                array('#[^a-zа-яё0-9/_"()-~]#iu', '/ё/u', '/Ё/u', '/\s+/'),
                array(' ', 'е', 'Е', ' '),
                $search_main));

        if (empty ($search_main)) {
            return $return;
        }

        $return['stages']['clear'] = $search_main;

        // NOT
        $search_main = preg_replace(array('/\s-(\S+)/u', '/\s~(\S+)/u'), '<NOT>$1', $search_main);
        $search_main = str_replace('-', ' ', $search_main);
        // QUOTE
        // Нечетное количество? Удаляем последнюю
        if ( preg_match_all('/"/u', $search_main, $dev_null)%2 ) {
            $search_main = preg_replace('/(.*)"(.*)/u', '$1 $2', $search_main, 1);
        }

        $search_main = preg_replace('/"(.+?)"/u', '<{>$1<}>', $search_main);

        // удаляем пустые кавычки
        $search_main = preg_replace('/<{>\s*<}>/', ' ', $search_main);

        // блоки вне кавычек и внутри
        $search_main = preg_split('/(<[{}]>)/u', $search_main, -1, PREG_SPLIT_DELIM_CAPTURE);

        // OR AND
        $in = false;
        foreach ($search_main as &$p) {
            if ($p === '<{>') {
                $in = true;
                continue;
            } elseif ($p === '<}>') {
                $in = false;
                continue;
            }

            if ( $in ) {
                // внутри
                $p = preg_replace('/[()-]/ui', ' ', $p);
                $word_br = preg_replace(array('/<.+?>/', '/[()-]/ui'), ' ', $p);
                $word_br = $this->text_del_short($word_br, $this->conf['search']['min_word_len']);
                $return['word_in_quote'][] = $word_br;
            } else {
                // снаружи кавычек
                // \b для русского не работает,
                do {
                    $p = preg_replace_callback('/\s(OR|AND|NOT|ИЛИ|НЕ|НЕТ)\s/ui',
                            create_function(
                            '$m',
                            'return mb_strtoupper(" <$m[1]> ");'
                            ), $p, -1, $count);
                } while ($count);
                $word_br = preg_replace(array('/<.+?>/', '/[()-]/ui'), ' ', $p);
                $word_br = $this->text_del_short($word_br, $this->conf['search']['min_word_len']);
                $return['word_out_quote'][] = $word_br;
            }
        }

        $to_m = array('<ИЛИ>' => '<OR>', '<НЕ>' => '<NOT>', '<НЕТ>' => '<NOT>');
        $search_main = str_replace(array_keys($to_m), array_values($to_m), $search_main);

        $search_main = implode('', $search_main);

        $return['stages']['before('] = $search_main;

        // СКОБКИ
        // пустые скобки
        $search_main = preg_replace('/\(\s*\)/u', ' ', $search_main);
        // на тэги, жадность, вложенные скобки преабразуются за счет цикла
        // НЕ ЖАДНОСТь
        // ("зимним олимпийским") (слово)
        // ("зимним олимпийским" (слово))
        do {
            $search_main = preg_replace('/\((.+?)\)/u', '<[>$1<]>', $search_main, -1, $count);
        } while ($count);
        // не парные скобки
        $search_main = preg_replace('/[()]/u', ' ', $search_main);
        $search_main = str_replace(array('[',']'), array('(',')'), $search_main);

        $return['stages']['('] = $search_main;

        // добавляем пробелов вокруг каждого тэга
        $search_main = preg_replace('/(<.*?>)/u', ' $1 ', $search_main);
        $search_main = preg_replace('/\s+/u', ' ', $search_main);

        // "OR AND" "AND OR" "AND OR OR" "OR NOT AND " "NOT OR" "NOT AND"
        $search_main = preg_replace(
                array(
                '/(<(OR|AND)> )(<(OR|AND)> )+/u', // несколько подряд в один
                '/(<NOT> )(<\w+> )+/u', // NOT перед другими любыми
                '/()^\s*<(OR|AND)>/u', // вначале, кроме NOT
                '/()<\w+>\s*$/u', // в конце
                ),
                '$1',
                $search_main);

        $return['stages'][] = $search_main;

        $search_main = $this->text_del_short($search_main, $this->conf['search']['min_word_len']);

        $search_main = preg_replace('/\s+/u', ' ', $search_main);

        // ("зимним олимпийским") (слово)
        // "оборудование" => оборудование
        // (оборудование) => оборудование
        $search_main = trim(preg_replace(
                array('/<\(>(\s[^<>\s]+\s)<\)>/u', '/<{>(\s[^<>\s]+\s)<}>/u'),
                ' $1 ', $search_main));

        // пустые тэги, окончательная очистка
        $search_main = trim(preg_replace(
                array('/<\(>\s*<\)>/u', '/<{>\s*<}>/u', '/\s+/u'),
                ' ', $search_main));

        $return['stages'][] = $search_main;
        $return['tag_form'] = $search_main;

        // Обратное преобразование
        $search_main = preg_replace(array('/<{>\s+/u', '/\s+<}>/u'), '"', $search_main);
        $search_main = preg_replace(array('/<\(>\s+/u', '/\s+<\)>/u'), array('(', ')'), $search_main);
        $search_main = preg_replace('/<NOT>\s+/u', '-', $search_main);
        $search_main = preg_replace('/<(OR|AND)>/u', '$1', $search_main);

        $return['clear'] = $search_main;

        return $return;
    }

    public function SetSearch(array $search = array()) {
        $this->search = $search;
        $this->clear_search();
    }

    public function query() {
        return $this->query_sphinx();
    }

    //select ROUND(sum(price)/1000000) from item ??
    //TODO
    //$search_main = array( 'region.name' );
    // FIXME создавать ключ по массиву search, не тратить время на расчеты
    //       аналогично и для total
    // пока не горит - все расчеты из индексов

    public function query_sphinx() {
        // TODO найстройки sphinx, количество коннектов
        // SHOW META => нельзя pconnect?
        // TODO разделение, обновление item_fresh
        // поиск по умолчанию лишь в item_fresh, item_delta
        // кэширование ошибочных!
        // ошибочная пагинация - если страница дальше, чем есть результатов => выводить последнюю
        // неизвестный ТИП

        $time_start = microtime(true);

        mb_internal_encoding('UTF-8');

        $this->clear_search();
        $this->search_query = $this->search;

        $str = '';

        $main = $this->sql_main_sphinx_lang();
        if ($main) {
            $str .= ' AND ' . $main;
        }

        $other = $this->sql_other_sphinx();
        if ($other) {
            $str .= ' AND ' . $other;
        }

        if (isset($this->search['param_no_null_price'])) {
            $str .= ' AND price<>0 ';
        }
        // TEST
        //$this->search_id_end   = 300100;
        //$this->search_id_start = 300000;
        if ($this->search_id_end) {
            // in Sphinx (M-id)
            $search_id_end = $this->sphinx_big_number - $this->search_id_start;
            $search_id_start = $this->sphinx_big_number - $this->search_id_end;
            $str .= " AND id >= $search_id_start AND id <= $search_id_end";
        }

        $str = preg_replace('/^AND/i', '', trim($str));

        $page = (int) @ $_GET['page'];
        $page = $page ? $page : 1;

//        if ($page > 99) {
//            $page = 99;
//            $_GET['page'] = $page;
//        }

        $where = '';
        if ($str) {
            $where = 'WHERE ' . trim($str);
        }

        $md5_count = md5("BETA:SEARCH:TOTAL:$where");
        if ($this->memcache_obj) {
            $total = $this->memcache_obj->get($md5_count);
            if (is_numeric($total)) {
                $total = (int) $total;
            }
        } else {
            $total = false;
        }

        // DISABLE
        $total = false;

        if (isset($this->onpage)) {
            $ppp = $this->onpage;
        } elseif (in_array(@$_GET['onpage'], $this->onpage_vals)) {
            $ppp = $_GET['onpage'];
        } elseif (in_array(@$_COOKIE['onpage'], $this->onpage_vals)) {
            $ppp = $_COOKIE['onpage'];
        } else {
            $ppp = $this->conf['pref']['ppp'];
        }

        $page = ($total !== FALSE && ceil($total / $ppp) < $page) ? ceil($total / $ppp) : $page;
        $LIMIT = 'LIMIT ' . ($page - 1) * $ppp . ", $ppp";

        $md5_items = md5("BETA:SEARCH:ITEMS:$where:$LIMIT");

        if ($this->memcache_obj) {
            $items = $this->memcache_obj->get($md5_items);
        } else {
            $items = false;
        }

        // DISABLE
        //$items = false;

        if ($total === 0) {
            $items = array();
        }

        if ($total === FALSE) {
            $items = false;
        }

        if ($items === FALSE) {
            // В sphinx 0.9.9 нет OR в Where, потому расчет идет в полях
            if (1) {
                $price_comp = '';
                if (isset($this->search['price_min']) || isset($this->search['price_max'])) {
                    $price_min = 0;
                    $price_max = 0;
                    if (isset($this->search['price_min'])) {
                        $price_min = (int) $this->search['price_min'];
                    }
                    if (isset($this->search['price_max'])) {
                        $price_max = (int) $this->search['price_max'];
                    }
                    if ($price_min && $price_max) {
                        $price_comp = "(price>=$price_min AND price<=$price_max)";
                    } elseif ($price_min) {
                        $price_comp = "(price>=$price_min)";
                    } elseif ($price_max) {
                        $price_comp = "(price<=$price_max)";
                    }
                    // TODO NULL??
                    /*if (isset($this->search['param_no_null_price'])) {
                        $price_null = '';
                    } else {
                        $price_null = 'price=0 OR ';
                    }*/
                    //$price_comp = ", ({$price_null}{$price_comp}) as price_filter";
                    $price_comp = ", ({$price_comp}) as price_filter";
                }
            }

            // FIXME уменьшить max_matches для маленьких результатов
            $sql = "SELECT *$price_comp" .
                    " FROM item_old, item_fresh, item_delta $where ORDER BY id ASC $LIMIT" .
                    " OPTION max_matches=5000";

            $time_sphinx = microtime(true);

            $this->search_error = '';
            try {
                $all = $this->sphinx->GetAll($sql);
            } catch (Exception $e) {
                $this->search_error = $e->getMessage();
                if ( preg_match('/\[.+?:.+?:(.+?)\]/', $this->search_error, $m) ) {
                    $this->search_error = trim($m[1]);
                }
                $all = array();
            }

            // FIXME как отрабатывает когда нет результатов
            // if (empty ($all) ) { }

            $this->mtime_sphinx = (int) ((microtime(true) - $time_sphinx) * 1000);

            $ids = array();

            foreach ($all as $v) {
                $ids[] = $this->sphinx_big_number - $v['id']; // in Sphinx (M-id)
            }

            // total_found уже посчитан, его всегда можно обновить
            $meta = array();
            try {
                $rawMeta = $this->sphinx->GetArray('SHOW META');
                foreach ($rawMeta as $v) {
                    $meta[$v['Variable_name']] = $v['Value'];
                }
            } catch (Exception $e) {
                $meta['total_found'] = 0;
            }
            $total = (int) $meta['total_found'];

            if ($total && $this->memcache_obj) {
                $this->memcache_obj->set($md5_count, $total, 0, $this->memcache_total);
            }

            $time_mysql = microtime(true);
            $items = $this->db_get_items($ids);
            $this->mtime_mysql = (int) ((microtime(true) - $time_mysql) * 1000);

            if ($this->memcache_obj) {
                $this->memcache_obj->set($md5_items, $items, 0, $this->memcache_items);
            }
        }

        // FIXME записывать время запросов
        $this->mtime = (int) ((microtime(true) - $time_start) * 1000);

        return array(
                'total' => $total,
                'items' => $items,
        );
    }

    /**
     * Поиск похожих через Sphinx
     * TODO: выбор ранкера
     * TODO: выбор весов для даты и релевантности
     * @param string $text текст, название для поиска
     * @param int    $id   сама позиция для исключения
     * @return array
     */
    public function similar_sphinx($text, $id=null) {
        mb_internal_encoding('UTF-8');

        $this->search = array();
        $this->search['main'] = $text;
        $this->search['param_or'] = 'on';
        $this->search['param_no_customer'] = true;
        $this->clear_search();

        $str = '';

        $main = $this->sql_main_sphinx_simple();
        $main = preg_replace('/@\(.*?\)/', '@(name,morf_name)', $main);
        if ($main) {
            $str .= ' AND ' . $main;
        }

        if ($id) {
            $str .= ' AND id <> ' . ($this->sphinx_big_number - $id);
        }

        $str = preg_replace('/^AND/i', '', trim($str));

        $time_start = microtime(true);

        $where = '';
        if ($str) {
            $where = 'WHERE ' . trim($str);
        }


        // search max weight
        $sql = "SELECT * FROM item_fresh, item_delta $where ORDER BY @weight DESC LIMIT 1 OPTION max_matches=5, ranker=$this->similar_sphinx_ranker";
        $max = $this->sphinx->GetAll($sql);
        if (isset($max[0]['weight'])) {
            $max = $max[0]['weight'];
        } else {
            return array();
        }

        $now = time();
        $time = strtotime("-$this->similar_sphinx_days day");

        $sql = "SELECT *, $this->similar_sphinx_weight*(@weight/$max) + " .
                "$this->similar_sphinx_date*((date - $time)/($this->similar_sphinx_days*24*60*60)) as my_weigth " .
                "FROM item_fresh, item_delta $where AND date < $now ORDER BY my_weigth DESC LIMIT 10 " .
                "OPTION max_matches=10, ranker=$this->similar_sphinx_ranker";

        $all = $this->sphinx->GetAll($sql);

        $ids = array();

        foreach ($all as $v) {
            $ids[] = $this->sphinx_big_number - $v['id']; // in Sphinx (M-id)
        }

        $items = $this->db_get_items($ids);

        // FIXME записывать время запросов
        $this->mtime = (int) ((microtime(true) - $time_start) * 1000);

        return $items;
    }

    protected function db_get_items(array $ids) {
        if (!empty($ids)) {
            // LEFT JOIN, ибо type_id может быть NULL
            $sql = 'SELECT item.*, type.name as type_name, type.sname as type_sname, site.name as site_name, region.name as region_name ' .
                    'FROM item ' .
                    'INNER JOIN site   ON item.site_id   =   site.id ' .
                    'LEFT  JOIN type   ON item.type_id   =   type.id ' .
                    'LEFT  JOIN region ON item.region_id = region.id ' .
                    'WHERE item.id IN (' . implode(', ', $ids) . ') ORDER BY id DESC';
            $items = $this->db->GetArray($sql);
        } else {
            $items = array();
        }
        return $items;
    }

    /**
     * Возврашает какие есть морфемы ввиде слово => МОРФЕМА
     * @param array $lemmas
     * @return array
     */
    private function get_morphemes(array $lemmas) {
        mb_internal_encoding('UTF-8');

        $return = array();

        if ( empty($lemmas) ) {
            return $return;
        }

        $main_morf = $this->main_to_morf(mb_strtolower(implode(' ', $lemmas)));

        foreach ($main_morf as $word => $lemma) {
            if ($word !== mb_strtolower($lemma)) {
                $return[$word] = mb_strtoupper($lemma);
            }
        }

        return $return;
    }


    /**
     * SQL
     * TODO кэшировать преобразование => брать из базы search?
     * @return array
     */
    protected function main_to_morf($main) {
        $m_str = morf_base_filter($main, false, false);

        if (empty($m_str)) {
            return $m_str;
        }

        $words = explode(' ', $m_str);
        foreach ($words as $word) {
            $words_all[crc_p($word)] = array(
                    'word' => $word,
                    'word_crc' => crc_p($word),
                    'lemma' => null,
                    'lemma_crc' => null,
            );
            $words_crc[] = crc_p($word);
        }

        // FIXME явный повтор кода
        $words_db = $this->db->GetArray(
                'SELECT crc, lemma_crc FROM morf_words WHERE crc IN (' . implode(',', $words_crc) . ') AND is_dict = 1');

        if (!empty($words_db)) {
            foreach ($words_db as $w) {
                $words_all[$w['crc']]['lemma_crc'] = $w['lemma_crc'];
                $lemma_crcs[] = $w['lemma_crc'];
            }
            $lemmas_db = $this->db->GetArray('SELECT crc, lemma FROM morf_lemmas WHERE crc IN (' . implode(',', $lemma_crcs) . ')');
            foreach ($lemmas_db as $l) {
                foreach ($words_all as &$w) {
                    if ($w['lemma_crc'] == $l['crc']) {
                        $w['lemma'] = $l['lemma'];
                        $w['lemma_crc'] = $l['crc'];
                        break;
                    }
                }
            }
        }

        $return = array();

        foreach ($words_all as &$w) {
            if (empty($w['lemma'])) {
                $return[$w['word']] = $w['word'];
            } else {
                $return[$w['word']] = mb_strtoupper($w['lemma'], 'utf-8');
            }
        }

        uksort($return, array($this, 'usort'));

        return $return;
    }

    /**
     * Преобразование языка запросов сайта в язык запросов Sphinx
     * Возвращает готовый MATCH(...) для Sphinx или пустую строку
     * FROM doc: queries that are non-computable
     *           -aaa
     *           aaa | -bbb
     * @return string
     */
    private function sql_main_sphinx_lang() {
        $search = $this->clear_search_main;
        mb_internal_encoding('UTF-8');
        $str = '';

        if (empty($search['tag_form'])) {
            return $str;
        }

        $for_morf = implode(' ', $search['word_out_quote']);
        $for_morf = array_unique(preg_split('/\s+/', trim($for_morf)));

        $morf = array();
        foreach ( $for_morf as $w ) {
            if ( empty($w) || preg_match('#[\d/_]#', $w) ) {
                continue;
            }
            $morf[] = $w;
        }

        $morf = $this->get_morphemes($morf);

        $in = false;
        foreach (explode(' ', $search['tag_form']) as $part) {
            switch ($part) {
                case '<NOT>':
                    $str .= ' -';
                    break;
                case '<(>':
                    $str .= '(';
                    break;
                case '<)>':
                    $str = trim(preg_replace('/\|\s*$/', '', $str));
                    $str .= ')';
                    break;
                case '<{>':
                    $in = true;
                    $str .= '"';
                    break;
                case '<}>':
                    $in = false;
                    $str = trim(preg_replace('/\|\s*$/', '', $str));
                    $str .= '" ';
                    break;
                case '<OR>':
                    $str = trim(preg_replace('/\|\s*$/', '', $str));
                    $str .= ' | ';
                    break;
                case '<AND>':
                    $str = trim(preg_replace('/\|\s*$/', '', $str));
                    $str .= ' ';
                    break;
                default:
                    if ($in) {
                        // внутки кавычек
                        $str .= "$part*";
                    } else {
                        // вне кавычек
                        if ( isset($morf[$part]) ) {
                            $str .= "($part* | $morf[$part]*)";
                        } else {
                            $str .= "$part*";
                        }
                    }

                    if ( isset($this->search['param_or']) ) {
                        $str .= " | ";
                    } else {
                        $str .= " ";
                    }
            }

        }
        $str = trim(preg_replace('/\|\s*$/', '', $str));

        $this->search_query['main'] = $str;
        if ($this->debug) {
            echo "<!--\n";
            print_r($this->search_query);
            echo "-->\n";
        }

        // escape
        $str = preg_replace('#/#', '\/', $str);

        $fileds = '';
        if (isset($this->search['param_no_customer'])) {
            $fileds = '@(name,morf_name,stuff,morf_stuff,num,morf_num) ';
        }

        $str = "MATCH('{$fileds}{$str}')";

        return $str;
    }


    /**
     * Очистка
     * @param <type> $text
     * @param <type> $len
     * @return string
     */
    private function text_del_short($text, $len=3) {
        mb_internal_encoding('UTF-8');
        $return = '';
        foreach (explode(' ', $text) as $v) {
            if (mb_strlen($v) >= $len) {
                $return .= $v . ' ';
            }
        }
        return trim($return);
    }


    /**
     * source AND morf together
     * @return string
     */
    private function sql_main_sphinx_simple() {
        if (!isset($this->search['main'])) {
            return '';
        }

        $main_morf = $this->main_to_morf($this->search['main']);

        // 'ПОСТАВКА* поставка*'

        $names_uniq = array();

        foreach ($main_morf as $word => $lemma) {
            if ($word === mb_strtolower($lemma, 'UTF-8')) {
                $names_uniq[] = "$lemma*";
            } else {
                $names_uniq[] = "($lemma* | $word*)";
            }
        }

        $return = '';

        $v_st = '';
        foreach ($names_uniq as $v) {
            if (isset($this->search['param_or'])) {
                $v_st .= " | $v";
            } else {
                $v_st .= " $v";
            }
        }
        $v_st = trim(preg_replace('/^\|/', '', trim($v_st)));

        $this->search_query['main'] = $v_st;

        if ($this->debug) {
            echo "<!--\n";
            print_r($this->search_query);
            echo "-->\n";
        }

        // escape
        $v_st = preg_replace('#/#', '\/', $v_st);

        $fileds = '';

        if (isset($this->search['param_no_customer'])) {
            $fileds = '@(name,morf_name,stuff,morf_stuff,num,morf_num) ';
        }

        $return = "MATCH('{$fileds}{$v_st}')";

        return $return;
    }

    protected function okr_to_regions(array $okrugs = array()) {
        $return_list = array();

        foreach ($this->search['okr'] as $okr => $val) {
            $return_list = array_merge($return_list, $this->all_data['okrug_region'][$okr]);
        }

        $return_list = array_unique($return_list);

        // TODO поиск по регином из main
        // ...
        // mysql оптимизирует и происходит связывание и тормоза...
        // item.region_id IN (
        // SELECT region.id FROM region WHERE region.okrug_id = $okr ) ";

        $okr_reg_list = array();

        foreach ($return_list as $reg) {
            $okr_reg_list[$reg] = $reg;
        }

        return $okr_reg_list;
    }

    /**
     * Добавляет подчиненные типы закупок
     * @param array $okrs
     * @return <type>
     */
    protected function type_to_types(array $types = array()) {
        $return_list = array();

        foreach ($this->search['type'] as $type_id => $val) {
            if ($type_id == 'gozall') {
                continue;
            }
            $return_list = array_merge($return_list, array($type_id));
            $return_list = array_merge($return_list, $this->all_data['type_parent'][$type_id]);
        }

        $return_list = array_unique($return_list);

        $okr_reg_list = array();

        foreach ($return_list as $id) {
            $okr_reg_list[$id] = $id;
        }

        return $okr_reg_list;
    }

    public function children_regions($regions) {
        $result = array();
        if (empty($regions)) {
            $regions[] = 100;
        }
        foreach ( $regions as $region ) {
            if ( $region % 100 == 0 ) {
                for ($i=($region-99); $i<=$region; $i++) {
                    if (isset($this->all_data['region'][$i])) {
                        $result[] = $i;
                    }
                }
            } else {
                $result[] = $region;
            }
        }
        return $result;
    }

    /**
     * SQL for Sphinx
     * @return string
     */
    protected function sql_other_sphinx() {
        $str = '';
        //TODO
        $regions_l = array();
        if (!empty($this->search['reg'])) {
            foreach ($this->search['reg'] as $reg => $val) {
                $regions_l[$reg] = $reg;
            }
        }

        if (!empty($this->search['okr'])) {
            $okr_reg_list = $this->okr_to_regions();
            $regions_l = array_merge($regions_l, $okr_reg_list);
        }

        $regions_l = $this->children_regions($regions_l);
        $regions_l = array_unique($regions_l);

        if (!empty($regions_l)) {
            sort($regions_l);
            $str .= ' AND region_id IN (' . implode(', ', $regions_l) . ')';
        }
        // если что-то есть в тайпе и это не гозол
        if (!empty($this->search['type']) && !isset($this->search['type']['gozall'])) {
            $type_list = $this->type_to_types();
            if (!empty($type_list)) {
                sort($type_list);
                $str .= ' AND type_id IN (' . implode(', ', $type_list) . ')';
            }
            // если есть и коммерческие, и государсвенные
        } elseif (isset($this->search['type'][100]) && isset($this->search['type']['gozall'])) {
            $this->search['type']['gozall'] = 'on';
            $this->search['type'][100] = 'on';
            // если ТОЛЬКО гозол
        } elseif (!empty ($this->search['type']) && sizeof($this->search['type']) == 1 && isset($this->search['type']['gozall'])) {
            $str .= ' AND type_id<>100';
            $this->search['type']['gozall'] = 'on';
        }

        if (!empty($this->search['rubric'])) {
            foreach ($this->search['rubric'] as $key => $val) {
                // выбираем все дочерние, если это корневой каталог
                if ($key < 100) {
                    foreach ($this->all_data['rubric'] as $id => $rubric) {
                        if ($rubric['parent'] == $key) {
                            $rubrics[] = $id;
                        }
                    }

                }
                else
                    $rubrics[] = $key;
            }
            if (!empty($rubrics)) {
                $rubrics = array_unique($rubrics);
                sort($rubrics);
                $str .= ' AND rubric_id IN (' . implode(', ', $rubrics) . ')';
            }
        }

        if (!empty($this->search['sector'])) {
            $str .= ' AND sector_id IN (' . implode(', ', array_keys($this->search['sector'])) . ')';
        }

        if (!empty($this->search['date_min'])) {
            preg_match('/(\d{2}).(\d{2}).(\d{4})/', $this->search['date_min'], $date_part);
            $date_min = gmmktime(0, 0, 0, $date_part[2], $date_part[1], $date_part[3]);
            $date_min = $date_min - 6 * 60 * 60;
            $str .= " AND date >= $date_min";
        }

        if (!empty($this->search['date_max'])) {
            preg_match('/(\d{2}).(\d{2}).(\d{4})/', $this->search['date_max'], $date_part);
            $date_max = gmmktime(0, 0, 0, $date_part[2], $date_part[1], $date_part[3]);
            $date_max = $date_max + 6 * 60 * 60;
            $str .= " AND date <= $date_max";
        }

        if (isset($this->search['price_min']) || isset($this->search['price_max'])) {
            // IN SEARCH
            $str .= ' AND price_filter > 0';
        }

        if (!empty($this->search['customer'])) {
            $str .= ' AND customer_id IN (' . implode(', ', array_keys($this->search['customer'])) . ')';
        }

        if (!empty($this->search['site'])) {
            $str .= ' AND site_id IN (' . implode(', ', array_keys($this->search['site'])) . ')';
        }

        $str = trim(preg_replace('/^AND/i', '', trim($str)));

        return $str;
    }

    public function to_info() {
        $str = '';

        if (isset($this->search['param_or'])) {
            $str .= ' c любым из слов,';
        }

        if (isset($this->search['param_no_customer'])) {
            $str .= ' не искать в заказчике,';
        }

        // FIXME Cache ME
        if (!empty($this->search['customer'])) {
            foreach ($this->search['customer'] as $id => $val) {
                if (($name = $this->db->GetOne("SELECT name FROM customer WHERE id = $id"))) {
                    $str .= " заказчик: «{$name}»,";
                }
            }
        }

        if (!empty($this->search['rubric'])) {
            foreach ($this->search['rubric'] as $id => $val) {
                if (($name = @$this->all_data['rubric'][$id]['name'])) {
                    $str .= " рубрика: «{$name}»,";
                }
            }
        }

        if (!empty($this->search['okr'])) {
            foreach ($this->search['okr'] as $okr => $val) {
                $str .= ' ' . $this->all_data['okrug'][$okr]['sname'] . ',';
            }
        }

        if (!empty($this->search['reg'])) {
            foreach ($this->search['reg'] as $reg => $val) {
                $str .= ' ' . $this->all_data['region'][$reg]['sname'] . ',';
            }
        }

        if (!empty($this->search['type'])) {
            if (isset($this->search['type']['gozall'])) {
                $str .= ' все госзакупки,';
            }
            foreach ($this->search['type'] as $type => $val) {
                if (isset($this->all_data['type'][$type]['name'])) {
                    $str .= " {$this->all_data['type'][$type]['name']},";
                }
            }
        } else {
            $str .= ' все госзакупки, Коммерческие тендеры,';
        }

        // price_min, price_max
        if (isset($this->search['price_min']) || isset($this->search['price_max'])) {
            if (isset($this->search['price_min'])) {
                $str .= ' от ' . $this->search['price_min'];
            }
            if (isset($this->search['price_max'])) {
                $str .= ' до ' . $this->search['price_max'];
            }
            $str .= ' т.р.,';
        }

        if (isset($this->search['param_no_null_price'])) {
            $str .= ' не выводить без цены,';
        }

        // date_min, date_max
        if (isset($this->search['date_min']) || isset($this->search['date_max'])) {
            $str .= ' публикация:';
            if (isset($this->search['date_min'])) {
                $str .= ' с ' . $this->search['date_min'];
            }
            if (isset($this->search['date_max'])) {
                $str .= ' до ' . $this->search['date_max'];
            }
            $str .= ',';
        }

        $str = preg_replace('/[.,\s]+$/', '', $str);

        return $str;
    }

    // Возвращает массив параметров поиска сгруппированный
    // по РЕГИОНАМ, КАТЕГОРИЯМ, ТИПАМ, ЗАКАЗЧИКАМ, и пр. параметрам
    public function to_info_param() {

        // Округа
        if (!empty($this->search['okr'])) {
            foreach ($this->search['okr'] as $okr => $val) {
                $result['reg'][100][$okr] = $this->all_data['okrug'][$okr]['sname'];
            }
        }
        // Регионы
        if (!empty($this->search['reg'])) {
            ksort($this->search['reg']);
            foreach ($this->search['reg'] as $reg => $val) {
                $parent_id = $this->all_data['region'][$reg]['parent_id'];
                if ($reg<100) {
                    $parent_id = 100;
                }
                if (!$parent_id) {
                    $parent_id = 0;
                }
                $okrug_id  = $this->all_data['region'][$reg]['okrug_id'];
                if ($parent_id && $okrug_id) {
                    $result['reg'][$parent_id][$okrug_id][$reg] = $this->all_data['region'][$reg]['sname'];
                }
                if ($parent_id && !$okrug_id) {
                    $result['reg'][$parent_id][$reg] = $this->all_data['region'][$reg]['sname'];
                }
                if (!$parent_id && !$okrug_id) {
                    $result['reg'][$reg] = $this->all_data['region'][$reg]['sname'];
                }
            }
        }
        if (!empty($result['reg'])) {
            $arr = array();
            foreach ($result['reg'] as $k1 => $l1) {
                $str1 = "";
                if (is_array($l1)) {
                    $str1 = $this->all_data['region'][$k1]['sname'] . "(";
                    $arr2 = array();
                    foreach ($l1 as $k2 => $l2) {
                        $str2 = "";
                        if (is_array($l2)) {
                            $str2 = $this->all_data['okrug'][$k2]['sname'] . "(";
                            $str2.= implode(", ", $l2);
                            $str2.= ")";
                        } else {
                            if ($k1==100) {
                                $str2 = $this->all_data['okrug'][$k2]['sname'];
                            } else {
                                $str2 = $this->all_data['region'][$k2]['sname'];
                            }
                        }
                        $arr2[] = $str2;
                    }
                    $str1.= implode(", ", $arr2) . ")";
                } else {
                    $str1 = $this->all_data['region'][$k1]['sname'];
                }
                $arr[] = $str1;
            }
            $result['reg'] = implode(", ", $arr);
        }

        // Типы
        if (!empty($this->search['type'])) {
            $result['type'] = array();
            if (isset($this->search['type']['gozall'])) {
                $result['type'][] = 'все госзакупки';
            }
            foreach ($this->search['type'] as $type => $val) {
                if (isset($this->all_data['type'][$type]['name'])) {
                    $result['type'][] = $this->all_data['type'][$type]['name'];
                }
            }
            if ((count($this->search['type'])==2) && (isset($this->search['type'][100])) && (isset($this->search['type']['gozall']))) {
                unset($result['type']);
            } else {
                $tc = count($result['type']);
                if ($tc>0) {
                    $list = "&laquo;".implode("&raquo;, &laquo;", $result['type']) . "&raquo;";
                    $result['type'] = array();
                    $result['type']['list'] = $list;
                    $result['type']['count'] = $tc;
                }
            }
        }

        // Рубрики
        if (!empty($this->search['rubric'])) {
            foreach ($this->search['rubric'] as $id => $val) {
                if (($name = @$this->all_data['rubric'][$id]['name'])) {
                    $result['rubric'][] = $name;
                }
            }
            $rc = count($result['rubric']);
            if ($rc>0) {
                $list = "&laquo;".implode("&raquo;, &laquo;", $result['rubric']). "&raquo;";
                $result['rubric'] = array();
                $result['rubric']['list'] = $list;
                $result['rubric']['count'] = $rc;
            }
        }

        // Заказчики
        if (!empty($this->search['customer'])) {
            $result['customer'] = array();
            foreach ($this->search['customer'] as $id => $val) {
                if (($name = $this->db->GetOne("SELECT name FROM customer WHERE id = $id"))) {
                    $result['customer'][] = $name;
                }
            }
            $cc = count($result['customer']);
            if ($cc>0) {
                $list = "&laquo;".implode("&raquo;, &laquo;", $result['customer']). "&raquo;";
                $result['customer'] = array();
                $result['customer']['list'] = $list;
                $result['customer']['count'] = $cc;
            }
        }

        // Дополнительные параметры
        // price_min, price_max
        $result['other'] = array();
        if (isset($this->search['price_min']) || isset($this->search['price_max'])) {
            $result['other']['price'] = 'начальная цена ';
            if (isset($this->search['price_min'])) {
                $result['other']['price'].= ' от ' . $this->search['price_min'];
            }
            if (isset($this->search['price_max'])) {
                $result['other']['price'].= ' до ' . $this->search['price_max'];
            }
            $result['other']['price'].= ' р.';
        }

        if (isset($this->search['param_no_null_price'])) {
            $result['other']['no_price'] = 'не выводить без цены';
        }

        // date_min, date_max
        if (isset($this->search['date_min']) || isset($this->search['date_max'])) {
            $result['other']['date'] = 'публикация:';
            if (isset($this->search['date_min'])) {
                $result['other']['date'] .= ' с ' . $this->search['date_min'];
            }
            if (isset($this->search['date_max'])) {
                $result['other']['date'] .= ' до ' . $this->search['date_max'];
            }
        }

        if (isset($this->search['param_or'])) {
            $result['other']['param_or'] = 'c любым из слов';
        }

        if (isset($this->search['param_no_customer'])) {
            $result['other']['param_no_customer'] = 'не искать в заказчике';
        }

        $oc = count($result['other']);
        if ($oc>0) {
            $list = implode(", ", $result['other']);
            $result['other'] = array();
            $result['other']['list'] = $list;
            $result['other']['count'] = $oc;
        } else {
            unset($result['other']);
        }

        return $result;
    }

    public function info_main() {
        $return = '';
        $form_info = $this->to_info();
        if (empty($form_info) && empty($this->search['main'])) {
            $return = '';
        } else {
            if (empty($this->search['main'])) {
                $return = $form_info;
            } else {
                if (empty($form_info)) {
                    $return = $this->search['main'];
                } else {
                    $return = $form_info . ', ' . $this->search['main'];
                }
            }
        }
        return $return;
    }

    public function to_hidden() {
        $hidden = array();
        foreach ($this->search as $key => $val) {
            if (is_array($val)) {
                foreach ($val as $key2 => $val2) {
                    $hidden[] = array(
                            'name' => "search[$key][$key2]",
                            'value' => 'on',
                    );
                }
            } else {
                $hidden[] = array(
                        'name' => 'search[' . $key . ']',
                        'value' => $val,
                );
            }
        }

        return $hidden;
    }

    public function to_url() {
        $url = '';
        foreach ($this->search as $key => $val) {
            if (is_array($val)) {
                foreach ($val as $key2 => $val2) {
                    $url .= rawurlencode("search[$key][$key2]") . '&';
                }
            } else {
                if ($val) {
                    $url .= rawurlencode("search[$key]") . '=' . rawurlencode($val) . '&';
                } else {
                    $url .= rawurlencode("search[$key]") . '&';
                }
            }
        }
        $url = preg_replace('/&$/i', '', $url);
        return $url;
    }

    public function to_string() {
        $return = '';
        foreach ($this->search as $key => $val) {
            if (is_array($val)) {
                foreach ($val as $key2 => $val2) {
                    $return .= "$key:$key2 ";
                }
            } else {
                if ($key == 'main') {
                    $return .= "$val ";
                } else {
                    $return .= "$key:$val ";
                }
            }
        }
        return $return;
    }

    /*
     * make array, what put in key data from subarray
     * input:  array( array('id' => 53, 'data' => 'some') );
     * output: array( 53 => array('id' => 53, 'data' => 'some') );
    */

    public function array_add_keys($array, $key = 'id') {
        $return = array();

        foreach ($array as $v) {
            $return[$v[$key]] = $v;
        }

        return $return;
    }

    protected function usort($a, $b) {
        $a = mb_strtolower($a, 'UTF-8');
        $b = mb_strtolower($b, 'UTF-8');

        if ($a === $b) {
            return 0;
        }

        $src = array($a, $b);
        $sort = $src;

        $oldLocale = setlocale(LC_COLLATE, '0');
        setlocale(LC_COLLATE, 'ru_RU.utf8');

        sort($sort, SORT_LOCALE_STRING);

        setlocale(LC_COLLATE, $oldLocale);

        if ($sort === $src) {
            return -1;
        } else {
            return 1;
        }
    }

    /**
     * Дополняем массив items
     * @param array $items
     * @return array
     */
    public function items_prepare(array $items, $wrap_name = null) {
        mb_internal_encoding('UTF-8');

        $search = $this->search;

        foreach ($items as &$item) {
            $item['link'] = $this->conf['pref']['link_detail'] . $item['id'];

            if (!empty($search['main'])) {
                $search['main'] = trim($search['main']);
                $text = implode(' ', array($item['name'], $item['stuff'], $item['num'], $item['customer'], $item['customer_address']));

                $search_main_new = array();

                foreach (explode(' ', $search['main']) as $w) {
                    $lcs = $this->morf_strlcs($w, $text);
                    if (mb_strlen($lcs) < 3) {
                        continue;
                    }
                    $search_main_new[] = $lcs;
                }

                if (empty($search_main_new)) {
                    $search_main_new_text = $search['main'];
                } else {
                    $search_main_new_text = implode(' ', $search_main_new);
                }

                $highlight = preg_replace("/\s+/", "|", preg_quote($search_main_new_text, '/'));

                $array = array(
                        'num' => '<br /><br /><b>Номер:</b> ',
                        'customer' => '<br /><br /><b>Заказчик:</b> ',
                        'customer_address' => '<br /><br /><b>Адрес заказчика:</b> ',
                );
                foreach ($array as $key => $pref) {
                    if (preg_match('/' . $highlight . '/xiu', $item[$key])) {
                        $item['name'] .= $pref . $item[$key];
                    }
                }
                $item['name'] = preg_replace("/$highlight/xiu", '<font color=#cc0000>\\0</font>', $item['name']);

                if (!preg_match('/' . $highlight . '/xiu', $item['name']) && preg_match('/' . $highlight . '/xiu', $item['stuff'])) {
                    $stuff = $this->items_prepare_cut_string($item['stuff'], $highlight);
                    $stuff = preg_replace("/$highlight/xiu", '<font color=#cc0000>\\0</font>', $stuff);
                    $item['name'] .= '<br /><br /><b>Позиция:</b> ' . $stuff;
                }
            }
        }

        return $items;
    }

    /**
     * вырезаем подходящие кусочки
     * FIXME добавить обрезку длиииных названий
     * @param string $text
     * @param string $highlight
     * @return string
     */
    private function items_prepare_cut_string($text, $highlight) {
        $text = preg_replace('/\s+/ui', ' ', $text);

        preg_match_all("/\S+/u", $text, $m, PREG_OFFSET_CAPTURE);
        $words = array();
        foreach ($m[0] as $one) {
            $words[] = array(
                    'word' => $one[0],
                    'pos' => (int) $one[1],
                    'end' => $one[1] + strlen($one[0]),
                    'len' => strlen($one[0]),
                    'status' => 0,
            );
        }

        preg_match_all("/$highlight/xiu", $text, $m, PREG_OFFSET_CAPTURE);
        foreach ($m[0] as $one) {
            $offset = $one[1];
            foreach ($words as &$w) {
                if ($offset <= $w['end'] && $offset >= $w['pos']) {
                    $w['status'] = 1;
                }
            }
        }

        unset($w);

        foreach ($words as $k => $w) {
            if ($w['status'] == 1) {
                if (isset($words[$k - 1]) && $words[$k - 1]['status'] != 1) {
                    $words[$k - 1]['status'] = 2;
                }
                if (isset($words[$k + 1]) && $words[$k + 1]['status'] != 1) {
                    $words[$k + 1]['status'] = 2;
                }
            }
        }

        foreach ($words as $k => $w) {
            if ($w['status'] == 0) {
                if (isset($words[$k - 1]) && isset($words[$k + 1])) {
                    if ($words[$k - 1]['status'] > 0 && $words[$k + 1]['status'] > 0) {
                        $words[$k]['status'] = 3;
                    }
                }
            }
        }

        $return = '';
        foreach ($words as $w) {
            if ($w['status'] > 0) {
                $return .= ' ' . $w['word'] . ' ';
            } else {
                $return .= '…';
            }
        }

        $return = preg_replace('/…+/ui', '…', $return);
        $return = preg_replace('/\s+/ui', ' ', $return);
        return $return;
    }

    /**
     * Наибольшая общая подстрока
     * @param string $str1
     * @param string $str2
     * @return string
     */
    private function morf_strlcs($str1, $str2) {
        mb_internal_encoding('UTF-8');
        return morf_strlcs($str1, $str2);
    }

    /**
     * Попытка перевода строки с ошибкой
     * @param string $string
     */
    public function translate_error($string) {
        if (preg_match('/node consists of NOT operators only/ui', $string)) {
            // query is non-computable (node consists of NOT operators only)
            $string = "запрос не корректен (в скобках все слова с отрицаниями)";
        } elseif  (preg_match("/single NOT operator/ui", $string)) {
            $string = "запрос не корректен (одно лишь отрицание)";
        } elseif (preg_match("/syntax error, unexpected '-' near/ui", $string)) {
            $string = preg_replace(
                    "/syntax error, unexpected '-' near/",
                    "ошибка синтаксиса, недопустимое отрицание рядом c",
                    $string);
        }
        return $string;
    }

    public function get_item_report($start=0, $count=100) {
        $sql = "SELECT *
                FROM item_report
                WHERE date_report > 1
                ORDER BY date_report DESC
                LIMIT ?,?";

        $all = $this->sphinx->GetAll($sql, array($start, $count));

        $meta = array();
        foreach ($this->sphinx->GetArray('SHOW META') as $v) {
            $meta[$v['Variable_name']] = $v['Value'];
        }
        $total = (int) $meta['total_found'];

        $ids = array();
        foreach ($all as $v) {
            $ids[] = $this->sphinx_big_number - $v['id']; // in Sphinx (M-id)
        }

        if (!empty($ids)) {
            // LEFT JOIN, ибо type_id может быть NULL
            $sql = 'SELECT item.site_id, item.type_id, item.region_id, item.price, item.id, item.name, item.date, item.date_end, type.name as type_name, type.sname as type_sname, site.name as site_name, region.name as region_name ' .
                    'FROM item ' .
                    'INNER JOIN site   ON item.site_id   =   site.id ' .
                    'LEFT  JOIN type   ON item.type_id   =   type.id ' .
                    'LEFT  JOIN region ON item.region_id = region.id ' .
                    'WHERE item.id IN (' . implode(', ', $ids) . ') ' .
                    'ORDER BY date_report DESC';
            $items = $this->db->GetArray($sql);
        } else {
            $items = array();
        }

        return array(
                'total' => $total,
                'items' => $items,
        );

    }

}
